iT邦幫忙

2025 iThome 鐵人賽

DAY 4
0

今天要做什麼?

昨天我們學會了 TDD 的紅綠重構循環,體驗了從無到有開發功能的完整流程。隨著測試越寫越多,你可能開始感到困擾:「這些測試散落各處,很難找到我要的測試」、「類似的測試重複出現,但又不完全一樣」。

想像一個場景:你的數學工具庫現在有 20 個方法,每個方法有 5-8 個測試案例,總共 100 多個測試。當某個測試失敗時,你需要在一大堆 it 中找到問題所在,這時你就會深刻體會到「測試結構」的重要性。

今天我們要學習如何組織測試,讓測試代碼變得清晰、有條理,就像整理房間一樣 —— 相關的東西放在一起,每樣東西都有固定位置。

學習目標

今天結束後,你將學會:

  • 掌握測試檔案的組織結構
  • 理解測試套件(Test Suite)的概念
  • 學會使用 describe/context 組織測試
  • 掌握測試命名的最佳實踐
  • 學會將測試分類和分組

TDD 學習地圖

第一階段:打好基礎(Day 1-10)
├── Day 01 - 環境設置與第一個測試
├── Day 02 - 認識斷言(Assertions)
├── Day 03 - TDD 紅綠重構循環
├── Day 04 - 測試結構與組織 ★ 今天在這裡
├── ...
└── (更多精彩內容待續)

為什麼需要測試結構? 🧮

散亂測試的痛點

回想昨天的數學工具庫,如果我們繼續用扁平的方式寫測試:

// 混亂的測試結構
it('isPrime with 2 should return true')
it('isPrime with 3 should return true') 
it('isPrime with 4 should return false')
it('countPrimesInRange with range 1-10')
it('isPrime with negative number should return false')
it('fibonacci with 0 should return 0')

問題:

  • 找不到相關測試isPrime 的測試散落各處
  • 缺乏邏輯分組:正向測試、邊界測試混在一起
  • 測試命名冗長:每個測試都要重複方法名
  • 難以維護:新增測試不知道放哪裡

結構化測試的好處

// 結構化的測試組織
describe('math utilities', function() {
    describe('isPrime', function() {
        describe('prime detection', function() {
            it('identifies small prime numbers');
            it('identifies large prime numbers');
        });
        
        describe('composite detection', function() {
            it('identifies small composite numbers');
            it('identifies large composite numbers');
        });
        
        describe('boundary cases', function() {
            it('handles negative numbers');
            it('handles zero and one');
        });
    });
});

好處:

  • 邏輯清晰:相關測試聚在一起
  • 階層分明:從大到小,從抽象到具體
  • 命名簡潔:測試名稱更精確
  • 易於維護:新測試有明確的歸屬

測試套件(Test Suite)概念 📝

測試套件是一組相關測試的集合,用來驗證特定功能或類別。在 Pest 中,我們用 describe 來創建測試套件:

describe('function name', function() {
    it('test case 1');
    it('test case 2');
});

測試套件的階層結構

describe('math utilities', function() {           // 頂層套件:類別
    describe('isPrime', function() {            // 第二層:方法
        describe('positive number tests', function() {       // 第三層:測試分類
            it('detects small prime numbers');                   // 具體測試案例
            it('detects large prime numbers');
        });
        
        describe('boundary tests', function() {
            it('handles zero');
            it('handles negative numbers');
        });
    });
});

這種結構讓測試報告更易讀:

數學工具庫
  ├── isPrime
  │   ├── 正數測試
  │   │   ✓ 檢測小質數
  │   │   ✓ 檢測大質數
  │   └── 邊界測試
  │       ✓ 處理零
  │       ✓ 處理負數

實戰演練:重構散亂的測試 🔧

讓我們把昨天的測試重新組織。建立 tests/Unit/Day04/MathUtilsTest.php

步驟 1:建立基本結構

<?php

use App\MathUtils;

describe('MathUtils', function() {
    describe('isPrime', function() {
        // 所有 isPrime 相關的測試都放這裡
    });
    
    describe('countPrimesInRange', function() {
        // 所有 countPrimesInRange 相關的測試都放這裡
    });
});

步驟 2:組織 isPrime 測試

建立 tests/Unit/Day04/MathUtilsTest.php

describe('isPrime', function() {
    describe('prime numbers', function() {
        it('identifies small primes', function() {
            expect(MathUtils::isPrime(2))->toBeTrue();
            expect(MathUtils::isPrime(3))->toBeTrue();
            expect(MathUtils::isPrime(5))->toBeTrue();
            expect(MathUtils::isPrime(7))->toBeTrue();
        });

        it('identifies larger primes', function() {
            expect(MathUtils::isPrime(11))->toBeTrue();
            expect(MathUtils::isPrime(13))->toBeTrue();
            expect(MathUtils::isPrime(17))->toBeTrue();
            expect(MathUtils::isPrime(19))->toBeTrue();
        });
    });

    describe('composite numbers', function() {
        it('identifies small composites', function() {
            expect(MathUtils::isPrime(4))->toBeFalse();
            expect(MathUtils::isPrime(6))->toBeFalse();
            expect(MathUtils::isPrime(8))->toBeFalse();
            expect(MathUtils::isPrime(9))->toBeFalse();
        });
    });

    describe('boundary cases', function() {
        it('handles numbers less than 2', function() {
            expect(MathUtils::isPrime(0))->toBeFalse();
            expect(MathUtils::isPrime(1))->toBeFalse();
            expect(MathUtils::isPrime(-1))->toBeFalse();
        });
    });
});

測試分組策略 📊

按功能分組

最常見的分組方式是按方法分組:

describe('MathUtils', function() {
    describe('isPrime', function() { /* ... */ });
    describe('fibonacci', function() { /* ... */ });
    describe('factorial', function() { /* ... */ });
});

按測試類型分組

在每個功能內,再按測試類型細分:

describe('isPrime', function() {
    describe('positive tests', function() {
        // 正常情況的測試
    });
    
    describe('boundary tests', function() {
        // 邊界值的測試
    });
    
    describe('error handling', function() {
        // 異常情況的測試(Day 8 會詳細學習)
    });
});

測試命名最佳實踐 🎯

命名原則

  1. 描述行為,不是實作
  2. 使用業務語言,不是技術術語
  3. 簡潔明確,避免冗長

好的命名範例

describe('isPrime', function() {
    // ✅ 好的命名:描述期望的行為
    it('identifies prime number 2');
    it('identifies composite number 4'); 
    it('handles negative numbers');
    
    // ❌ 不好的命名:過於技術性
    it('should return true when input is 2');
});

推薦命名模式:[動詞] + [對象] + [條件]

實戰範例:完整測試結構

完整實作 tests/Unit/Day04/MathUtilsTest.php

<?php

use App\MathUtils;

describe('MathUtils', function() {
    describe('isPrime', function() {
        describe('prime numbers', function() {
            it('identifies small primes', function() {
                expect(MathUtils::isPrime(2))->toBeTrue();
                expect(MathUtils::isPrime(3))->toBeTrue();
                expect(MathUtils::isPrime(5))->toBeTrue();
                expect(MathUtils::isPrime(7))->toBeTrue();
            });

            it('identifies larger primes', function() {
                expect(MathUtils::isPrime(11))->toBeTrue();
                expect(MathUtils::isPrime(13))->toBeTrue();
                expect(MathUtils::isPrime(17))->toBeTrue();
                expect(MathUtils::isPrime(19))->toBeTrue();
            });
        });

        describe('composite numbers', function() {
            it('identifies small composites', function() {
                expect(MathUtils::isPrime(4))->toBeFalse();
                expect(MathUtils::isPrime(6))->toBeFalse();
                expect(MathUtils::isPrime(8))->toBeFalse();
                expect(MathUtils::isPrime(9))->toBeFalse();
            });

            it('identifies larger composites', function() {
                expect(MathUtils::isPrime(10))->toBeFalse();
                expect(MathUtils::isPrime(12))->toBeFalse();
                expect(MathUtils::isPrime(14))->toBeFalse();
                expect(MathUtils::isPrime(15))->toBeFalse();
            });
        });

        describe('boundary cases', function() {
            it('handles numbers less than 2', function() {
                expect(MathUtils::isPrime(0))->toBeFalse();
                expect(MathUtils::isPrime(1))->toBeFalse();
                expect(MathUtils::isPrime(-1))->toBeFalse();
                expect(MathUtils::isPrime(-10))->toBeFalse();
            });
        });
    });

    describe('countPrimesInRange', function() {
        describe('valid ranges', function() {
            it('counts primes in small range', function() {
                expect(MathUtils::countPrimesInRange(1, 10))->toBe(4);
            });

            it('counts primes in medium range', function() {
                expect(MathUtils::countPrimesInRange(10, 20))->toBe(4);
            });
        });

        describe('edge cases', function() {
            it('handles single number range', function() {
                expect(MathUtils::countPrimesInRange(2, 2))->toBe(1);
                expect(MathUtils::countPrimesInRange(4, 4))->toBe(0);
            });

            it('handles reversed range', function() {
                expect(MathUtils::countPrimesInRange(10, 1))->toBe(0);
            });
        });
    });
});

執行測試查看結構

./vendor/bin/pest tests/Unit/Day04/MathUtilsTest.php --verbose

輸出結果會顯示清晰的階層:

MathUtils
  isPrime
    prime numbers
      ✓ identifies small primes
      ✓ identifies larger primes
    composite numbers  
      ✓ identifies small composites
      ✓ identifies larger composites
    boundary cases
      ✓ handles numbers less than 2
  countPrimesInRange
    valid ranges
      ✓ counts primes in small range
      ✓ counts primes in medium range
    edge cases
      ✓ handles single number range
      ✓ handles reversed range

Tests: 9 passed

今天學到什麼? 🎉

今天我們從散亂的測試進化到有組織的測試結構:

技術收穫

  • 掌握測試套件概念:用 describe 創建階層結構
  • 學會測試分組策略:按功能、類型、場景分組
  • 掌握命名最佳實踐:清晰、簡潔、具描述性
  • 理解檔案組織原則:結構一致性

組織能力

  • 從混亂到有序:重新組織成清晰結構
  • 邏輯分類:相關測試聚在一起
  • 結構思考:先規劃再實作

實用技巧

  • 階層設計:模組 > 功能 > 測試類型 > 具體案例
  • 命名規範:動詞 + 對象 + 條件的命名模式
  • 結構思考:先規劃再實作,相關測試聚在一起

總結

測試結構與組織不只是美觀,更是實用。良好的組織結構能提高開發效率、降低維護成本、改善團隊協作。記住:好的測試結構是可維護測試代碼的基礎。

明天我們將學習「測試生命週期」,了解如何在測試執行前後進行必要的設置和清理工作。 💪


上一篇
Day 03 - TDD 紅綠重構循環
下一篇
Day 05 - 測試生命週期 🔄
系列文
Laravel Pest TDD 實戰:從零開始的測試驅動開發9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言